﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;

namespace XmlVisualizer.DiagramDesigner
{
    /// <summary>
    /// Represents connection between two controls.
    /// </summary>
    public class Connection : Control, ISelectable, INotifyPropertyChanged
    {
        #region Fields
        private Adorner connectionAdorner;
        private Connector source;
        private Connector sink;
        private PathGeometry pathGeometry;
        
        // between source connector position and the beginning 
        // of the path geometry we leave some space for visual reasons; 
        // so the anchor position source really marks the beginning 
        // of the path geometry on the source side
        private Point anchorPositionSource;
        
        // slope of the path at the anchor position
        // needed for the rotation angle of the arrow
        private double anchorAngleSource = 0;
        
        // analogue to the source side
        private Point anchorPositionSink;
        
        // analogue to the source side
        private double anchorAngleSink = 0;
        private ArrowSymbol sourceArrowSymbol = ArrowSymbol.None;
        private ArrowSymbol sinkArrowSymbol = ArrowSymbol.Arrow;
        
        // specifies a point at half path length
        private Point labelPosition;
        
        // pattern of dashes and gaps that is used to outline the connection path
        private DoubleCollection strokeDashArray;
        
        // if connected, the ConnectionAdorner becomes visible
        private bool isSelected;
        #endregion

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the Connection class. Sets the source and sink connectors.
        /// </summary>
        /// <param name="source">Source connector.</param>
        /// <param name="sink">Sink connector.</param>
        /// <exception cref="System.ArgumentNullException">Source or sink is null.</exception>
        public Connection(Connector source, Connector sink)
        {
            if (source == null)
            {
                throw new ArgumentNullException("source");
            }

            if (sink == null)
            {
                throw new ArgumentNullException("sink");
            }

            this.ID = Guid.NewGuid();
            this.Source = source;
            this.Sink = sink;
            this.Unloaded += new RoutedEventHandler(this.Connection_Unloaded);
        }
        #endregion

        #region INotifyPropertyChanged Members
        /// <summary>
        /// PropertyChanged event.
        /// </summary> 
        public event PropertyChangedEventHandler PropertyChanged;        
        #endregion

        #region Properties
        /// <summary>
        /// Gets or sets the ID.
        /// </summary>
        public Guid ID { get; set; }

        /// <summary>
        /// Gets or sets the source connector.
        /// </summary>
        public Connector Source
        {
            get
            {
                return this.source;
            }

            set
            {
                // if (value != null) {
                if (this.source != value)
                {
                    if (this.source != null)
                    {
                        this.source.PropertyChanged -= new PropertyChangedEventHandler(this.OnConnectorPositionChanged);
                        this.source.Connections.Remove(this);
                    }

                    this.source = value;

                    if (this.source != null)
                    {
                        this.source.Connections.Add(this);
                        this.source.PropertyChanged += new PropertyChangedEventHandler(this.OnConnectorPositionChanged);
                    }

                    this.UpdatePathGeometry();
                }
            }
        }

        /// <summary>
        /// Gets or sets the sink connector.
        /// </summary>
        public Connector Sink
        {
            get
            {
                return this.sink;
            }

            set
            {
                if (this.sink != value)
                {
                    if (this.sink != null)
                    {
                        this.sink.PropertyChanged -= new PropertyChangedEventHandler(this.OnConnectorPositionChanged);
                        this.sink.Connections.Remove(this);
                    }

                    this.sink = value;

                    if (this.sink != null)
                    {
                        this.sink.Connections.Add(this);
                        this.sink.PropertyChanged += new PropertyChangedEventHandler(this.OnConnectorPositionChanged);
                    }

                    this.UpdatePathGeometry();
                }
            }
        }

        /// <summary>
        /// Gets or sets the path geometry.
        /// </summary>
        public PathGeometry PathGeometry
        {
            get
            {
                return this.pathGeometry;
            }

            set
            {
                if (this.pathGeometry != value)
                {
                    this.pathGeometry = value;
                    this.UpdateAnchorPosition();
                    this.OnPropertyChanged("PathGeometry");
                }
            }
        }

        /// <summary>
        /// Gets or sets the source anchor position.
        /// Between source connector position and the beginning 
        /// of the path geometry we leave some space for visual reasons
        /// so the anchor position source really marks the beginning 
        /// of the path geometry on the source side.
        /// </summary>
        public Point AnchorPositionSource
        {
            get
            {
                return this.anchorPositionSource;
            }

            set
            {
                if (this.anchorPositionSource != value)
                {
                    this.anchorPositionSource = value;
                    this.OnPropertyChanged("AnchorPositionSource");
                }
            }
        }

        /// <summary>
        /// Gets or sets the source anchor angle.
        /// Slope of the path at the anchor position
        /// needed for the rotation angle of the arrow.
        /// </summary>
        public double AnchorAngleSource
        {
            get
            {
                return this.anchorAngleSource;
            }

            set
            {
                if (this.anchorAngleSource != value)
                {
                    this.anchorAngleSource = value;
                    this.OnPropertyChanged("AnchorAngleSource");
                }
            }
        }

        /// <summary>
        /// Gets or sets the sink anchor position.
        /// </summary>
        public Point AnchorPositionSink
        {
            get
            {
                return this.anchorPositionSink;
            }

            set
            {
                if (this.anchorPositionSink != value)
                {
                    this.anchorPositionSink = value;
                    this.OnPropertyChanged("AnchorPositionSink");
                }
            }
        }

        /// <summary>
        /// Gets or sets the sink anchor angle.
        /// </summary>
        public double AnchorAngleSink
        {
            get
            {
                return this.anchorAngleSink;
            }

            set
            {
                if (this.anchorAngleSink != value)
                {
                    this.anchorAngleSink = value;
                    this.OnPropertyChanged("AnchorAngleSink");
                }
            }
        }

        /// <summary>
        /// Gets or sets the source arrow symbol.
        /// </summary>
        public ArrowSymbol SourceArrowSymbol
        {
            get
            {
                return this.sourceArrowSymbol;
            }

            set
            {
                if (this.sourceArrowSymbol != value)
                {
                    this.sourceArrowSymbol = value;
                    this.OnPropertyChanged("SourceArrowSymbol");
                }
            }
        }

        /// <summary>
        /// Gets or sets the sink arrow symbol.
        /// </summary>
        public ArrowSymbol SinkArrowSymbol
        {
            get
            {
                return this.sinkArrowSymbol;
            }

            set
            {
                if (this.sinkArrowSymbol != value)
                {
                    this.sinkArrowSymbol = value;
                    this.OnPropertyChanged("SinkArrowSymbol");
                }
            }
        }

        /// <summary>
        /// Gets or sets the label position.
        /// </summary>
        public Point LabelPosition
        {
            get
            {
                return this.labelPosition;
            }

            set
            {
                if (this.labelPosition != value)
                {
                    this.labelPosition = value;
                    this.OnPropertyChanged("LabelPosition");
                }
            }
        }

        /// <summary>
        /// Gets or sets the stroke dash array.
        /// </summary>
        public DoubleCollection StrokeDashArray
        {
            get
            {
                return this.strokeDashArray;
            }

            set
            {
                if (this.strokeDashArray != value)
                {
                    this.strokeDashArray = value;
                    this.OnPropertyChanged("StrokeDashArray");
                }
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether the Connection is selected.
        /// </summary>
        public bool IsSelected
        {
            get
            {
                return this.isSelected;
            }

            set
            {
                if (this.isSelected != value)
                {
                    this.isSelected = value;
                    this.OnPropertyChanged("IsSelected");
                    if (this.isSelected)
                    {
                        this.ShowAdorner();
                    }
                    else
                    {
                        this.HideAdorner();
                    }
                }
            }
        }
        #endregion        

        #region Methods
        /// <summary>
        /// Deletes the connection.
        /// </summary>
        public void Delete()
        {
            this.Source = null;
            this.Sink = null;

            // remove adorner
            if (this.connectionAdorner != null)
            {
                DesignerCanvas designer = VisualTreeHelper.GetParent(this) as DesignerCanvas;

                AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(this);
                if (adornerLayer != null)
                {
                    adornerLayer.Remove(this.connectionAdorner);
                    this.connectionAdorner = null;
                }
            }
        }
        #endregion       

        #region Event handlers
        /// <summary>
        /// Called when the property changes.
        /// </summary>
        /// <param name="name">Property name.</param>
        protected void OnPropertyChanged(string name)
        {
            PropertyChangedEventHandler handler = this.PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(name));
            }
        }

        /// <summary>
        /// OnMouseDown event handler.
        /// </summary>
        /// <param name="e">The event data.</param>
        protected override void OnMouseDown(System.Windows.Input.MouseButtonEventArgs e)
        {
            base.OnMouseDown(e);

            // usual selection business
            DesignerCanvas designer = VisualTreeHelper.GetParent(this) as DesignerCanvas;
            if (designer != null)
            {
                if ((Keyboard.Modifiers & (ModifierKeys.Shift | ModifierKeys.Control)) != ModifierKeys.None)
                {
                    if (this.IsSelected)
                    {
                        designer.selectionService.RemoveFromSelection(this);
                    }
                    else
                    {
                        designer.selectionService.AddToSelection(this);
                    }
                }
                else if (!this.IsSelected)
                {
                    designer.selectionService.SelectItem(this);
                }

                // Focus(); // causes scrollviewer to scroll if enabled
            }

            e.Handled = false;
        }

        /// <summary>
        /// OnConnectorPositionChanged event handler.
        /// </summary>
        /// <param name="sender">The object where the event handler is attached.</param>
        /// <param name="e">The event data.</param>
        private void OnConnectorPositionChanged(object sender, PropertyChangedEventArgs e)
        {
            // whenever the 'Position' property of the source or sink Connector 
            // changes we must update the connection path geometry
            if (e.PropertyName.Equals("Position"))
            {
                this.UpdatePathGeometry();
            }
        }

        /// <summary>
        /// Handler for connection unloaded event.
        /// </summary>
        /// <param name="sender">The object where the event handler is attached.</param>
        /// <param name="e">The event data.</param>
        private void Connection_Unloaded(object sender, RoutedEventArgs e)
        {
            // do some housekeeping when Connection is unloaded

            // remove event handler
            // this.Source = null;
            // this.Sink = null;

            // // remove adorner
            // if (this.connectionAdorner != null) {
            //    DesignerCanvas designer = VisualTreeHelper.GetParent(this) as DesignerCanvas;

            //    AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(this);
            //    if (adornerLayer != null) {
            //        adornerLayer.Remove(this.connectionAdorner);
            //        this.connectionAdorner = null;
            //    }
            // }
        }
        #endregion

        #region Private methods
        /// <summary>
        /// Updates path geometry.
        /// </summary>
        private void UpdatePathGeometry()
        {
            if (this.Source != null && this.Sink != null)
            {
                PathGeometry geometry = new PathGeometry();
                List<Point> linePoints = PathFinder.GetConnectionLine(this.Source.GetInfo(), this.Sink.GetInfo(), true);
                if (linePoints.Count > 0)
                {
                    PathFigure figure = new PathFigure();
                    figure.StartPoint = linePoints[0];
                    linePoints.Remove(linePoints[0]);
                    figure.Segments.Add(new PolyLineSegment(linePoints, true));
                    geometry.Figures.Add(figure);

                    this.PathGeometry = geometry;
                }
            }
        }

        /// <summary>
        /// Updates anchor position.
        /// </summary>
        private void UpdateAnchorPosition()
        {
            Point pathStartPoint, pathTangentAtStartPoint;
            Point pathEndPoint, pathTangentAtEndPoint;
            Point pathMidPoint, pathTangentAtMidPoint;

            // the PathGeometry.GetPointAtFractionLength method gets the point and a tangent vector 
            // on PathGeometry at the specified fraction of its length
            this.PathGeometry.GetPointAtFractionLength(0, out pathStartPoint, out pathTangentAtStartPoint);
            this.PathGeometry.GetPointAtFractionLength(1, out pathEndPoint, out pathTangentAtEndPoint);
            this.PathGeometry.GetPointAtFractionLength(0.5, out pathMidPoint, out pathTangentAtMidPoint);

            // get angle from tangent vector
            this.AnchorAngleSource = Math.Atan2(-pathTangentAtStartPoint.Y, -pathTangentAtStartPoint.X) * (180 / Math.PI);
            this.AnchorAngleSink = Math.Atan2(pathTangentAtEndPoint.Y, pathTangentAtEndPoint.X) * (180 / Math.PI);

            // add some margin on source and sink side for visual reasons only
            pathStartPoint.Offset(-pathTangentAtStartPoint.X * 5, -pathTangentAtStartPoint.Y * 5);
            pathEndPoint.Offset(pathTangentAtEndPoint.X * 5, pathTangentAtEndPoint.Y * 5);

            this.AnchorPositionSource = pathStartPoint;
            this.AnchorPositionSink = pathEndPoint;
            this.LabelPosition = pathMidPoint;
        }

        /// <summary>
        /// Shows connection adorner.
        /// </summary>
        private void ShowAdorner()
        {
            // the ConnectionAdorner is created once for each Connection
            if (this.connectionAdorner == null)
            {
                DesignerCanvas designer = VisualTreeHelper.GetParent(this) as DesignerCanvas;

                AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(this);
                if (adornerLayer != null)
                {
                    this.connectionAdorner = new ConnectionAdorner(designer, this);
                    adornerLayer.Add(this.connectionAdorner);
                }
            }

            this.connectionAdorner.Visibility = Visibility.Visible;
        }

        /// <summary>
        /// Hides connection adorner.
        /// </summary>
        private void HideAdorner()
        {
            if (this.connectionAdorner != null)
            {
                this.connectionAdorner.Visibility = Visibility.Collapsed;
            }
        }
        #endregion
    }
}